/*
 * @(#)CollaborativeFiltering.java        2011/01/31
 *
 */

package fr.ups.jim.rdf.tp2;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Collections;

/**
 * @author Ji MA (Universite Paris Sud (ji.ma@u-psud.fr))
 * @author Antoine Sylvain (Universite Paris Sud (antoinesyl@gmail.com))
 * @version 1.0a
 */
public class CollaborativeFiltering {

	private Map<String, Map> critics;
	
	/**
	 * Default Constructor for initializing dataset
	 */
	public CollaborativeFiltering(){
		critics = new HashMap<String, Map>();
		
		/* ratings of "Leonard Hofstadter" */
		Map<String, Double> rating1 = new HashMap<String, Double>();
		rating1.put("2001 : L'Odyssée de l'espace", 2.5);
		rating1.put("Blade Runner", 3.5);
		rating1.put("Starwars", 3.0);
		rating1.put("Metropolis", 3.5);
		rating1.put("Brazil", 2.5);
		rating1.put("Matrix", 3.0);
		critics.put("Leonard Hofstadter", rating1);
		
		/* ratings of "Sheldon Cooper" */ 
		Map<String, Double> rating2 = new HashMap<String, Double>();
		rating2.put("2001 : L'Odyssée de l'espace", 3.0);
		rating2.put("Blade Runner", 3.5);
		rating2.put("Starwars", 1.5);
		rating2.put("Metropolis", 5.0);
		rating2.put("Brazil", 3.5);
		critics.put("Sheldon Cooper", rating2);
		
		/* ratings of "Penny" */
		Map<String, Double> rating3 = new HashMap<String, Double>();
		rating3.put("2001 : L'Odyssée de l'espace", 2.5);
		rating3.put("Blade Runner", 3.0);
		rating3.put("Metropolis", 3.5);
		rating3.put("Matrix", 4.0);
		critics.put("Penny", rating3);
		
		/* ratings of "Howard Wolowitz" */
		Map<String, Double> rating4 = new HashMap<String, Double>();
		rating4.put("Blade Runner", 3.5);
		rating4.put("Starwars", 3.0);
		rating4.put("Matrix", 4.5);
		rating4.put("Metropolis", 4.0);
		rating4.put("Brazil", 2.5);
		critics.put("Howard Wolowitz", rating4);
		
		/* ratings of "Rajesh Koothrappali" */
		Map<String, Double> rating5 = new HashMap<String, Double>();
		rating5.put("2001 : L'Odyssée de l'espace", 3.0);
		rating5.put("Blade Runner", 4.0);
		rating5.put("Starwars", 2.0);
		rating5.put("Metropolis", 3.0);
		rating5.put("Matrix", 3.0);
		rating5.put("Brazil", 2.0);
		critics.put("Rajesh Koothrappali", rating5);
		
		/* ratings of "Leslie Winkle" */
		Map<String, Double> rating6 = new HashMap<String, Double>();
		rating6.put("2001 : L'Odyssée de l'espace", 3.0);
		rating6.put("Blade Runner", 4.0);
		rating6.put("Matrix", 3.0);
		rating6.put("Metropolis", 5.0);
		rating6.put("Brazil", 3.5);
		critics.put("Leslie Winkle", rating6);
		
		/* rating of "Toby" */
		Map<String, Double> rating7 = new HashMap<String, Double>();
		rating7.put("Blade Runner", 4.5);
		rating7.put("Brazil", 1.0);
		rating7.put("Metropolis", 4.0);
		critics.put("Toby", rating7);
	}

	/**
	 * For question 1, this method reads in a corpus and returns a hashmap
	 * which binds every user with his/her reatings for two films 
	 * @param corpus contains all users' ratings for all films
	 * @return a hashmap which contains every user's ratings for specific
	 * 		   films, in this case, two films : "2001" and "Blade Runner"
	 */
	public static Map<String, ArrayList<Double>> 
			bindUserRatings(Map<String, Map> corpus) {
		// selected user ratings for specific films
		Map<String, ArrayList<Double>> selectedRatings = 
									new HashMap<String, ArrayList<Double>>();
		
		for (String name : corpus.keySet()) {
			ArrayList<Double> userRatings = new ArrayList<Double>();
			
			// add rating of "2001 : L'Odyssee de l'espace"
			if (corpus.get(name).get("2001 : L'Odyssée de l'espace") == null)
				userRatings.add(Double.valueOf(0));
			else 
				userRatings.add(Double.valueOf( corpus.get(name).get
								("2001 : L'Odyssée de l'espace").toString()));
			
			// add rating of "Blade Runner"
			if (corpus.get(name).get("Blade Runner") == null)
				userRatings.add(Double.valueOf(0));
			else
				userRatings.add(Double.valueOf( corpus.get(name).get
								("Blade Runner").toString()));
			
			selectedRatings.put(name, userRatings);
		}
		
		return selectedRatings;
	}
	
	/**
	 * Auxiliary function for question 4
	 * @param ratings	a corpus of ratings of every user for all films
	 * @param person1	name of the first person
	 * @param person2	name of the second person
	 * @return 			common movies of given two persons	
	 */
	public static ArrayList<String> commonMovies(
			Map<String, Map> ratings, 
			String person1, 
			String person2) {
	
		ArrayList<String> commonMovies = new ArrayList<String>();
		Map<String, Double> rating1 = 
			(HashMap<String, Double>) ratings.get(person1);
		Map<String, Double> rating2 = 
			(HashMap<String, Double>) ratings.get(person2);
		
		for(String movie : rating1.keySet()) {
			if (rating2.containsKey(movie))
				commonMovies.add(movie);
		}
		
		return commonMovies;
	}

	/**
	 * For question 4, calculate the Euclidean Distance between two persons
	 * @param ratings	a corpus of ratings of every user for all films
	 * @param person1	name of the first person
	 * @param person2	name of the second person
	 * @return double	Euclidean Distance between two persons 
	 */
	public static double euclideanDistance(
			Map<String, Map> ratings, 
			String person1, 
			String person2) {
		
		ArrayList<String> commonMovies = CollaborativeFiltering.commonMovies
										(ratings, person1, person2);
		double distance = 0.0;
		Map<String, Double> rating1 = 
			(HashMap<String, Double>) ratings.get(person1);
		Map<String, Double> rating2 = 
			(HashMap<String, Double>) ratings.get(person2);
		
		for (String movie : commonMovies) {
			distance += Math.pow(
						(rating1.get(movie).doubleValue()
						- rating2.get(movie).doubleValue()), 2);
		}
		
		distance = Math.sqrt(distance);
		return distance;
	}
	
	/**
	 * For question 6, return n nearest persons to the given selector(person)
	 * @param ratings	a corpus of ratings of every user for all films
	 * @param person	selected person's name
	 * @param n			the number of nearest persons to be returned
	 * @return 			n nearest persons to the person provided 
	 */
	public static List<DistanceNode> nearestPersons(
			Map<String, Map> ratings, String person, int n) {
		List<DistanceNode> nearestPersons = new ArrayList<DistanceNode>();
		double distance;
		
		for (String otherPerson : ratings.keySet()) {
			if (!otherPerson.equals(person)) {
				distance = CollaborativeFiltering.euclideanDistance(
						ratings, person, otherPerson);
				nearestPersons.add(new DistanceNode(otherPerson, distance));
			}
		}
		
		// sort ArrayList according to every item's distance value
		Collections.sort(nearestPersons, new DistanceNodeComparator());
		
		if (n >= nearestPersons.size())
			return nearestPersons;
		else
			return nearestPersons.subList(0, n);
	}
	
	/**
	 * For question 8, calculate Pearson correlation coefficient
	 * @param ratings	a corpus of ratings of every user for all films
	 * @param person1	name of the first person
	 * @param person2	name of the second person
	 * @return 			Pearson correlation coefficient of two given persons
	 */
	public static double calculatePearsonCC(Map<String, Map> ratings,
			String person1, String person2) {
		double pearsonCC, ratingAvg1, ratingAvg2;
		double distance1, distance2;
		int filmNum;
		
		ArrayList<String> commonFilms = CollaborativeFiltering.commonMovies
				(ratings, person1, person2);
		
		// get all ratings of person1 and person2
		Map<String, Double> ratings1 = (HashMap<String, Double>) 
										ratings.get(person1);
		Map<String, Double> ratings2 = (HashMap<String, Double>)
										ratings.get(person2);
		
		// calculate average rating of person1 and person2 for common movies
		ratingAvg1 = 0.0;
		ratingAvg2 = 0.0;
		filmNum = commonFilms.size();
		for (String film : commonFilms) {
			ratingAvg1 += ratings1.get(film).doubleValue();
			ratingAvg2 += ratings2.get(film).doubleValue();
		}
		ratingAvg1 = ratingAvg1 / filmNum;
		ratingAvg2 = ratingAvg2 / filmNum;
		
		// calculate person1's distance to its average rating
		distance1 = 0.0;
		distance2 = 0.0;
		for (String film : commonFilms) {
			distance1 += Math.pow((ratings1.get(film) - ratingAvg1), 2);
			distance2 += Math.pow((ratings2.get(film) - ratingAvg2), 2);
		}
		distance1 = Math.sqrt(distance1);
		distance2 = Math.sqrt(distance2);
		
		// calculate the Pearson Correlation Coefficient
		pearsonCC = 0.0;
		String tempFilm;
		for(int i = 0; i < commonFilms.size(); i++) {
			tempFilm = commonFilms.get(i);
			pearsonCC += (ratings1.get(tempFilm) - ratingAvg1)
						*(ratings2.get(tempFilm) - ratingAvg2);
		}
		pearsonCC = pearsonCC / (distance1 * distance2);
		
		return pearsonCC;
	}
	
	
	/**
	 * Calculate N nearsest person based on the PCC similarity
	 * @param ratings	a corpus of ratings of every user for all films
	 * @param person	selected person's name
	 * @param n			the number of nearest persons to be returned
	 * @return 			n nearest persons to the person provided 
	 */
	public static List<DistanceNode> nearPersonsPCC(			
			Map<String, Map> ratings, String person, int n) {
		List<DistanceNode> nearestPersonsPCC = new ArrayList<DistanceNode>();
		double pccValue;
		
		for (String otherPerson : ratings.keySet()) {
			if (!otherPerson.equals(person)) {
				pccValue = CollaborativeFiltering.calculatePearsonCC(
						ratings, person, otherPerson);
				nearestPersonsPCC.add(new DistanceNode(otherPerson, pccValue));
			}
		}
		
		// sort ArrayList according to every item's distance value
		Collections.sort(nearestPersonsPCC, new DistanceNodeComparator());
		Collections.reverse(nearestPersonsPCC);
		
		if (n >= nearestPersonsPCC.size())
			return nearestPersonsPCC;
		else
			return nearestPersonsPCC.subList(0, n);
	}
	
	
	/**
	 * Calculate a score for a specific film for a given user
	 * This method adopts "Euclidean Distance" as simliarity function
	 * @param ratings	a corpus of ratings of all users for all films
	 * @param usrName	calculate a score for this particular user
	 * @param filmName	calculate a score for this particular film
	 * @return			score calculated
	 */
	public static double calculateScore(Map<String, Map> ratings,
			String usrName, String filmName) {
		double score = 0.0;
		double distance, note;				// distance and note of other users
		Map<String, Double> usrRatings;		// a random user's ratings
		
		for (String name : ratings.keySet()) {
			if (!(usrName.equals(name))) {
				usrRatings = (HashMap<String, Double>) ratings.get(name);
				
				// get one other user's note for given film
				if (usrRatings.containsKey(filmName)) 
					note = usrRatings.get(filmName);
				else
					note = 0.0;
				
				// get the distance between current user and given user
				distance = CollaborativeFiltering.euclideanDistance(ratings, 
						usrName, name);
				
				score += distance * note;
			}
		}
		
		return score;
	}
	
	
	/**
	 * Calculate a score for a specific film for a given user
	 * This method uses "Pearson Correlation Coefficient" as simliarity function
	 * @param ratings	a corpus of ratings of all users for all films
	 * @param usrName	calculate a score for this particular user
	 * @param filmName	calculate a score for this particular film
	 * @return			score calculated
	 */
	public static double calculateScorePCC(Map<String, Map> ratings,
			String usrName, String filmName) {
		double score = 0.0;
		double similarity, note;	    	// distance and note of other users
		Map<String, Double> usrRatings;		// a random user's ratings
		
		for (String name : ratings.keySet()) {
			if (!(usrName.equals(name))) {
				usrRatings = (HashMap<String, Double>) ratings.get(name);
				
				// get one other user's note for given film
				if (usrRatings.containsKey(filmName)) 
					note = usrRatings.get(filmName);
				else
					note = 0.0;
				
				// get the similarity between current user and given user
				similarity = CollaborativeFiltering.calculatePearsonCC
					(ratings, usrName, name);
				
				score += similarity * note;
			}
		}
		
		return score;
	}
	
	
	/**
	 * For question 12, return n films which could be appreciated by given user
	 * @param ratings	a corpus of ratings of all users for all films
	 * @param usrName	films to be recommended to this person
	 * @param n			the number of films to be recommended
	 * @return 			n films associated with their scores
	 */
	public static List<DistanceNode> possibleLikeFilms (
			Map<String, Map> ratings, String usrName, int n) {
		List<DistanceNode> filmsRecommended = 
			new ArrayList<DistanceNode>();
		
		Map<String, Double> usrRatings = 
			(HashMap<String, Double>) ratings.get(usrName);
		
		ArrayList<String> filmsCounted = new ArrayList<String>();
		for (String user : ratings.keySet()) {
			for (String film : ((HashMap<String, Double>) 
									ratings.get(user)).keySet()) {
				// ensure the given user has not been noted for this film
				// also ensure this film has not been calculated
				if (!usrRatings.containsKey(film) 
						&& !filmsCounted.contains(film)) {
					filmsCounted.add(film);
					DistanceNode filmRec = new DistanceNode(film, 
							calculateScore(ratings, usrName, film));
					filmsRecommended.add(filmRec);
				}
			}
		}
		
		Collections.sort(filmsRecommended, new DistanceNodeComparator());
		
		if (filmsRecommended.size() <= n)
			return filmsRecommended;
		else
			return filmsRecommended.subList(0, n);
	}
	

	/**
	 * For question 12, return n films which could be appreciated by given user
	 * @param ratings	a corpus of ratings of all users for all films
	 * @param usrName	films to be recommended to this person
	 * @param n			the number of films to be recommended
	 * @return 			n films associated with their scores
	 */
	public static List<DistanceNode> possibleLikeFilmsPCC (
			Map<String, Map> ratings, String usrName, int n) {
		List<DistanceNode> filmsRecommended = 
			new ArrayList<DistanceNode>();
		
		Map<String, Double> usrRatings = 
			(HashMap<String, Double>) ratings.get(usrName);
		
		ArrayList<String> filmsCounted = new ArrayList<String>();
		for (String user : ratings.keySet()) {
			for (String film : ((HashMap<String, Double>) 
									ratings.get(user)).keySet()) {
				// ensure the given user has not been noted for this film
				// also ensure this film has not been calculated
				if (!usrRatings.containsKey(film) 
						&& !filmsCounted.contains(film)) {
					filmsCounted.add(film);
					DistanceNode filmRec = new DistanceNode(film, 
							calculateScorePCC(ratings, usrName, film));
					filmsRecommended.add(filmRec);
				}
			}
		}
		
		Collections.sort(filmsRecommended, new DistanceNodeComparator());
		
		// because films, with higher score, are recommended, therefore,
		// reverse the order for facility 
		Collections.reverse(filmsRecommended);
		
		if (filmsRecommended.size() <= n)
			return filmsRecommended;
		else
			return filmsRecommended.subList(0, n);
	}
	
	
	/**
	 * For question 14, evaluate the results given by euclidean distance
	 * @param ratings	a corpus of ratings of all users for all films
	 * @param usrName	films to be recommended to this person
	 */
	public static void evaluateQuality (
			Map<String, Map> ratings, String usrName) {
		List<DistanceNode> filmsRecommended = 
			new ArrayList<DistanceNode>();
		
		ArrayList<String> filmsCounted = new ArrayList<String>();
		for (String user : ratings.keySet()) {
			for (String film : ((HashMap<String, Double>) 
									ratings.get(user)).keySet()) {

				if (!filmsCounted.contains(film)) {
					filmsCounted.add(film);
					DistanceNode filmRec = new DistanceNode(film, 
							calculateScore(ratings, usrName, film));
					filmsRecommended.add(filmRec);
				}
			}
		}
		
		Collections.sort(filmsRecommended, new DistanceNodeComparator());
		
		System.out.println("-----------real order----------------");
		
		ArrayList<DistanceNode> filmsViewedList = new ArrayList<DistanceNode>();
		Map<String, Double> filmsViewed = 
			(HashMap<String, Double>) ratings.get(usrName);
		for (String filmName : filmsViewed.keySet()) {
			DistanceNode d = new DistanceNode();
			d.setUserName(filmName);
			d.setDistance(filmsViewed.get(filmName).doubleValue());
			filmsViewedList.add(d);
		}
		Collections.sort(filmsViewedList, new DistanceNodeComparator());
		for (DistanceNode t : filmsViewedList) {
			System.out.println(t.getUserName());
		}
	
		System.out.println("------------recommended order------------------");
		for (DistanceNode d : filmsRecommended) {
			if (filmsViewed.containsKey(d.getUserName()))
				System.out.println(d.getUserName());
		}
		
	}
	
	
	/**
	 * For question 14, evaluate the results by Pearson Correlation Coefficient
	 * @param ratings	a corpus of ratings of all users for all films
	 * @param usrName	films to be recommended to this person
	 */
	public static void evaluateQualityPCC (
			Map<String, Map> ratings, String usrName) {
		List<DistanceNode> filmsRecommended = 
			new ArrayList<DistanceNode>();
		
		ArrayList<String> filmsCounted = new ArrayList<String>();
		for (String user : ratings.keySet()) {
			for (String film : ((HashMap<String, Double>) 
									ratings.get(user)).keySet()) {

				if (!filmsCounted.contains(film)) {
					filmsCounted.add(film);
					DistanceNode filmRec = new DistanceNode(film,
							calculateScorePCC(ratings, usrName, film));
					filmsRecommended.add(filmRec);
				}
			}
		}
		
		Collections.sort(filmsRecommended, new DistanceNodeComparator());
		Collections.reverse(filmsRecommended);
		
		System.out.println("-----------real order----------------");
		
		ArrayList<DistanceNode> filmsViewedList = new ArrayList<DistanceNode>();
		Map<String, Double> filmsViewed = 
			(HashMap<String, Double>) ratings.get(usrName);
		for (String filmName : filmsViewed.keySet()) {
			DistanceNode d = new DistanceNode();
			d.setUserName(filmName);
			d.setDistance(filmsViewed.get(filmName).doubleValue());
			filmsViewedList.add(d);
		}
		Collections.sort(filmsViewedList, new DistanceNodeComparator());
		for (DistanceNode t : filmsViewedList) {
			System.out.println(t.getUserName());
		}
	
		System.out.println("------------recommended order------------------");
		for (DistanceNode d : filmsRecommended) {
			if (filmsViewed.containsKey(d.getUserName()))
				System.out.println(d.getUserName());
		}
		
	}
	
	public static void main(String[] args){
		CollaborativeFiltering cf = new CollaborativeFiltering();

		/* test dataset correctness */
//		for (String name : cf.critics.keySet()) {
//			System.out.println(name);
//			System.out.println(cf.critics.get(name));
//		}
		
		
		/* test for Question 1*/
//		Map<String, ArrayList<Double>> selectedRatings = 
//			CollaborativeFiltering.bindUserRatings(cf.critics);
//		for (String name : selectedRatings.keySet()) {
//			System.out.println(name + " : " + selectedRatings.get(name));
//		}
		
		
		/* test for method commonMovies : 
		 * test result : Metropolis, Blade Runner*/
//		ArrayList<String> commonMovies = CollaborativeFiltering.commonMovies
//									(cf.critics, "Penny", "Toby");
//		for (String movie : commonMovies)
//			System.out.println(movie);
		
		
		/* test for Question 4 : test result : 1.58*/
//		double euclideanDistance = CollaborativeFiltering.euclideanDistance
//								(cf.critics, "Penny", "Howard Wolowitz");
//		System.out.println(euclideanDistance);
		
		
		/* test for Question 6  */
//		List<DistanceNode> nearestPersons = 
//				CollaborativeFiltering.nearestPersons(cf.critics, "Penny", 4);
//		for(DistanceNode t : nearestPersons) {
//			System.out.println(t.getUserName() + " : " + t.getDistance());
//		}
		
		
		/* test for Question 8  */
//		ArrayList<String> usrName = new ArrayList<String>();
//		double pearsonCC = 0.0;
//		for (String name : cf.critics.keySet())
//			usrName.add(name);
//		
//		for (int i = 0; i < usrName.size() - 1; i++) {
//			for (int j = i + 1; j < usrName.size(); j++) {
//				pearsonCC = CollaborativeFiltering.calculatePearsonCC
//							(cf.critics, usrName.get(i), usrName.get(j));
//				System.out.print("PCC for "
//						+ usrName.get(i) + " and " + usrName.get(j) 
//						+ " is ");
//				System.out.printf("%.3f \n", pearsonCC);
//			}
//		}
		
		
//		/* test for Question 10 */
//		List<DistanceNode> nearestPersons = 
//			CollaborativeFiltering.nearestPersons(cf.critics, "Penny", 6);
//		List<DistanceNode> nearestPersonsPCC = 
//			CollaborativeFiltering.nearPersonsPCC(cf.critics, "Penny", 6);
//		
//		for (int i = 0; i < nearestPersons.size(); i++) {
//			System.out.print(nearestPersons.get(i).getUserName() + " ");
//			System.out.printf("%.4f", nearestPersons.get(i).getDistance());
//			System.out.print(" | "+nearestPersonsPCC.get(i).getUserName()+" ");
//			System.out.printf("%.4f", nearestPersonsPCC.get(i).getDistance());
//			System.out.print("\n");
//		}
		
		
		/* test for Question 12 */
//		List<DistanceNode> films = 
//			CollaborativeFiltering.possibleLikeFilms(cf.critics, "Toby", 3);
//		for (DistanceNode d : films) {
//			System.out.println(d.getUserName() + " : " + d.getDistance());
//		}
		
//		List<DistanceNode> films = 
//			CollaborativeFiltering.possibleLikeFilmsPCC(cf.critics, "Toby", 3);
//		for (DistanceNode d : films) {
//			System.out.println(d.getUserName() + " : " + d.getDistance());
//		}
		
		
		/* test for Question 14 */
//		CollaborativeFiltering.evaluateQuality(cf.critics, "Penny");
//		CollaborativeFiltering.evaluateQualityPCC(cf.critics, "Penny");
	}
}

/**
 * This auxiliary class is used contains only two private filed, which 
 * indicates its user name and distance between another user.
 */
class DistanceNode {
	private String name;
	private double distance;
	
	public DistanceNode(){
		setUserName(null);
		setDistance(0);
	}
	public DistanceNode(String name, double dis){
		setUserName(name);
		setDistance(dis);
	}
	
	public String getUserName() {
		return name;
	}
	
	public void setUserName(String name) {
		this.name = name;
	}
	
	public double getDistance() {
		return distance;
	}
	
	public void setDistance(double dis) {
		distance = dis;
	}
}

/**
 * This class is particularly used for defining the sorting rules for 
 * DistanceNode.
 */
class DistanceNodeComparator implements Comparator<DistanceNode> {
	public int compare(DistanceNode p1, DistanceNode p2) {
		if (p1.getDistance() > p2.getDistance())
			return 1;
		else
			return 0;
	}
}
